/**
 * PHOSIDE: PHosphorylation Site IDentification Engine.
 * Copyright 2009 Digital Biology Lab, University of Missouri.
 * This library is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License as published by the Free Software
 * Foundation; either version 3 of the License, or (at your option) any later
 * version. <p/> This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the License for more
 * details.
 */

package phoside;

import cytoscape.task.TaskMonitor;

import java.io.File;
import java.io.IOException;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.TreeSet;

import phoside.PhosphoProteins.PhosphoType;

import phoside.classify.AverageBoostingBinaryClassifier;
import phoside.classify.BoostingBinaryClassifier;
import phoside.classify.BootstrapBinaryClassifier;
import phoside.classify.FeatureLinearNormalizer;
import phoside.classify.FrequencyFeatureExtractorTrainingDataChangeHandler;
import phoside.classify.KNNFeatureExtractorTrainingDataChangeHandler;
import phoside.classify.SVMLightBinaryClassifier;
import phoside.classify.result.ROC;

import phoside.data.feature.extaction.AminoAcidFrequencyFeatureExtractor;
import phoside.data.feature.extaction.DisorderFeatureExtractor;
import phoside.data.feature.extaction.FeatureExtractor;
import phoside.data.feature.extaction.InstancesExtractor;
import phoside.data.feature.extaction.InstancesExtractorFromProteins;
import phoside.data.feature.extaction.KNNFeatureExtractor;
import phoside.data.feature.model.Instance;
import phoside.data.util.PhosphoDataUtil;
import phoside.data.util.ProteinProcessor;

import phoside.knn.ClosenessCalculator;
import phoside.knn.KNNExtractor;
import phoside.knn.KNNExtractorImpl;
import phoside.knn.SequenceBasedClosenessCalculator;
import phoside.knn.matrix.similarity.MatrixNormalizer;
import phoside.knn.matrix.similarity.MatrixNormalizerLinear;
import phoside.knn.matrix.similarity.MatrixUtils;
import phoside.knn.matrix.similarity.SimilarityMatrix;

import phoside.model.PhosideModel;
import phoside.model.PhosideModelImpl;

import phoside.util.CrossValidation;
import phoside.util.Pair;
import phoside.util.StaticFinalValues;

/**
 *
 * @author gjj
 */
public class PhosideTrain {

    private TaskMonitor monitor;

    public void setTaskMonitor(TaskMonitor monitor) {
        this.monitor = monitor;
    }

    private void setMonitorStatus(String status) {
        if (monitor!=null) {
            monitor.setStatus(status);
        }
    }

    public void interrupt() {
        
    }

    /**
     * 
     * @param dirFasta
     * @param dirModel
     * @param dirDisorder
     * @param dirDisorderSaveto
     * @param param
     * @throws IOException
     */
    public PhosideModel train(PhosphoProteins phosphoData,
                         Set<PhosphoType> types,
                         Properties props) throws IOException {

        int maxOffset = 0;
        
        String svm_option = props.getProperty(StaticFinalValues.SVMPARAMETERS);
        int nboots = Integer.parseInt(props.getProperty(StaticFinalValues.NOOFBOOTS));
        int nclassifiers = Integer.parseInt(props.getProperty(StaticFinalValues.NOOFCLASSIFIERS));

        // knn param
        final boolean useKNN = props.getProperty(StaticFinalValues.USEKNNFEATURES).equalsIgnoreCase("true");
        final String dirMatrix = PhosideInit.matrixDir.getAbsolutePath()+File.separator+props.getProperty(StaticFinalValues.SUBSTITUTIONMATRIX);
        SimilarityMatrix similarityMatrix = MatrixUtils.readBLASTMatrix(dirMatrix);
        MatrixNormalizer matrixNormalizer = new MatrixNormalizerLinear();
        MatrixUtils.normalizeMatrix(similarityMatrix, matrixNormalizer);
        int knnWindowOffset = Integer.parseInt(props.getProperty(StaticFinalValues.KNNWINDOWSIZE))/2;
        if (useKNN && knnWindowOffset>maxOffset) {
            maxOffset = knnWindowOffset;
        }
        String strKs = props.getProperty(StaticFinalValues.KNNNEIGHBORSIZES);
        String[] strs = strKs.split(",");
        int[] Ks = new int[strs.length];
        for (int i=0; i<strs.length; i++) {
            Ks[i] = (int)(Double.parseDouble(strs[i].trim())*2*nboots*0.01);
        }

        // disorder param
        final boolean useDisorder = props.getProperty(StaticFinalValues.USEDISORDERFEATURES).equalsIgnoreCase("true");
        String strDisWindows = props.getProperty(StaticFinalValues.DISORDERWINDOWSIZES);
        strs = strDisWindows.split(",");
        int[] disWindowOffsets = new int[strs.length];
        for (int i=0; i<strs.length; i++) {
            disWindowOffsets[i] = Integer.parseInt(strs[i].trim())/2;
            if (useDisorder && disWindowOffsets[i]>maxOffset) {
                maxOffset = disWindowOffsets[i];
            }
        }

        // frequency param
        final boolean useFrequency = props.getProperty(StaticFinalValues.USEFREQUENCYFEATURES).equalsIgnoreCase("true");
        int frequencyNumAA = Integer.parseInt(props.getProperty(StaticFinalValues.FREQUENCYFEATURENUMBER));
        int frequencyWindow = Integer.parseInt(props.getProperty(StaticFinalValues.FREQUENCYWINDOWSIZE))/2;
        if (useFrequency && frequencyWindow>maxOffset) {
            maxOffset = frequencyWindow;
        }

        // filter out proteins without disorder information
        final int minLen = maxOffset*2+1;
        phosphoData.filterProteins(new ProteinFilter() {
            public boolean filter(Protein protein) {
                String seq = protein.getSequence();
                if (seq==null || seq.length()<minLen) {
                    return false;
                }

                if (useDisorder) {
                    Object obj = protein.getInfo(StaticFinalValues.DISORDER);
                    if (obj==null)
                        return false;

                    if (((List<Double>)obj).isEmpty())
                        return false;
                }

                return true;
            }
        });

        // padding
        final boolean padding = props.getProperty(StaticFinalValues.PADDINGTERMINALS).equalsIgnoreCase("true");

        // extract instances
        System.out.println("Extracting positive instance...");
        setMonitorStatus("Extracting positive instance...");
        InstancesExtractorFromProteins insExtractor = new InstancesExtractorFromProteins(phosphoData, null, types, padding?-1:maxOffset, InstancesExtractor.PhosphoTag.PHOSPHO);
        List<Instance> instances_positive = insExtractor.fetch(-1);
        System.out.println("  "+instances_positive.size()+" instances were extracted");
        System.out.println("Extracting unlabeled instance...");
        setMonitorStatus("Extracting unlabeled instance...");
        double extractRatio = phosphoData.proteinCount()<=10000?1.0:10000.0/phosphoData.proteinCount(); // to save memory
        insExtractor = new InstancesExtractorFromProteins(phosphoData, null, types, padding?-1:maxOffset, InstancesExtractor.PhosphoTag.NONPHOSPHO, extractRatio);
        List<Instance> instances_unlabeled = insExtractor.fetch(-1);
        System.out.println("  "+instances_unlabeled.size()+" instances were extracted");

        // separate training and control
        List<Instance> positive_train = instances_positive;
        int npos_train = positive_train.size();

        int nneg_control = Integer.parseInt(props.getProperty(StaticFinalValues.NEGATIVECONTROLSIZE));
        if (nneg_control>instances_unlabeled.size()/2) {
            nneg_control = instances_unlabeled.size()/2;
        }
        Pair<List,List> ptt = CrossValidation.seperateTrainingTestSet(instances_unlabeled, nneg_control);
        List<Instance> negative_train = ptt.getFirst();
        int nneg_train = negative_train.size();
        List<Instance> negative_control = ptt.getSecond();

        List<Instance> instances_train = new ArrayList<Instance>(positive_train);
        instances_train.addAll(negative_train);

        List<Double> labels_train = new ArrayList(npos_train+nneg_train);
        for (int i=0; i<npos_train; i++) {
            labels_train.add(1.0);
        }
        for (int i=0; i<nneg_train; i++) {
            labels_train.add(-1.0);
        }

        // set up feature extractor
        FeatureExtractor disorderExtractor = new DisorderFeatureExtractor(disWindowOffsets, true);

        ClosenessCalculator closenessCalculator = new SequenceBasedClosenessCalculator(similarityMatrix, knnWindowOffset);
        KNNExtractor nnExtractor = new KNNExtractorImpl(closenessCalculator);

        FeatureLinearNormalizer featureNormalizer = new FeatureLinearNormalizer(-1.0,1.0);

        // setup classifier
        BoostingBinaryClassifier boostingClassifier = new AverageBoostingBinaryClassifier();
        for (int ic=0; ic<nclassifiers; ic++) {
            List<FeatureExtractor> extractors = new ArrayList<FeatureExtractor>(3);
            if (useDisorder) {
                extractors.add(disorderExtractor);
            }

            AminoAcidFrequencyFeatureExtractor frequencyExtractor = new AminoAcidFrequencyFeatureExtractor(frequencyWindow);
            FrequencyFeatureExtractorTrainingDataChangeHandler freTrainingDataChangeHandler = null;
            if (useFrequency) {
                extractors.add(frequencyExtractor);
                freTrainingDataChangeHandler = new FrequencyFeatureExtractorTrainingDataChangeHandler(
                        frequencyExtractor, frequencyNumAA, frequencyWindow);
            }

            KNNFeatureExtractorTrainingDataChangeHandler knnTrainingDataChangeHandler = null;
            if (useKNN) {
                KNNFeatureExtractor knnExtractor = new KNNFeatureExtractor(Ks, nnExtractor);
                extractors.add(knnExtractor);
                knnTrainingDataChangeHandler = new KNNFeatureExtractorTrainingDataChangeHandler(knnExtractor);
            }


            //set up classifier
            SVMLightBinaryClassifier svmClassifier = new SVMLightBinaryClassifier(svm_option, featureNormalizer, extractors);

            BootstrapBinaryClassifier bootstrapClassifier = new BootstrapBinaryClassifier(svmClassifier, nboots);

            if (useKNN) {
                bootstrapClassifier.addTrainingDataChangeHandler(knnTrainingDataChangeHandler);
            }

            if (useFrequency) {
                bootstrapClassifier.addTrainingDataChangeHandler(freTrainingDataChangeHandler);
            }

            //svmClassifier.train(instances_train, labels_train);
            //bootstrapClassifier.train(instances_train, labels_train);
            boostingClassifier.addClassifier(bootstrapClassifier);

        }

        System.out.println("Training...");
        setMonitorStatus("Training classifier...");
        boostingClassifier.train(instances_train, labels_train);

        
        // evaluating
        ROC roc = null;

        System.out.println("Evaluating...");
        setMonitorStatus("Evaluating model...");
        List<Double> prediction = boostingClassifier.classify(negative_control);

        roc = new ROC(prediction);

        PhosideModel model = new PhosideModelImpl(types, boostingClassifier, props, roc);

        return model;
    }
}
